// Copyright (C) 2010 Razvan Constantin <razvan.constantin@rconst.net>
//
// This file is part of FileBow.
//
// FileBow is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// FileBow is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with FileBow.  If not, see <http://www.gnu.org/licenses/>.

#include "logic/path-scanner.h"

#include <queue>

#include <glib.h>
#include <Poco/DirectoryIterator.h>
#include <Poco/File.h>
#include <Poco/Path.h>

#include "common/common.h"
#include "logic/path-tree.h"

using namespace std;
using Poco::DirectoryIterator;
using Poco::File;
using Poco::Path;

namespace filebow {

class PathScanner::Impl {
public:
  Impl() : mMutex(g_mutex_new()), mThread(NULL) {
  }
  ~Impl() {
    g_mutex_free(mMutex);
  }
  void Lock() {
    g_mutex_lock(mMutex);
  }
  void Unlock() {
    g_mutex_unlock(mMutex);
  }
  void Join() {
    GThread* thread = NULL;
    Lock();
    thread = mThread;
    Unlock();
    if (thread) {
      g_thread_join(thread);
    }
  }
  static gpointer async_scan_thread(gpointer data) {
    PathScanner* scanner = (PathScanner*)data;
    scanner->InternalScan();
    scanner->pimpl->Lock();
    scanner->pimpl->mThread = NULL;
    scanner->pimpl->Unlock();
  }

  GMutex* mMutex;
  GThread* mThread;
};

/**
 * Container for a path to be scanned with additional info.
 */
struct PathEntry {
  // Actual path.
  Path path;
  // Depth relative to scanned path.
  int depth;

  // Constructor.
  PathEntry(const Path& iPath, int iDepth) : path(iPath), depth(iDepth) {}
};

PathScanner::PathScanner(const Path& path)
    : pimpl(new Impl), mPath(path), mMaxDepth(-1), mMaxFiles(-1) {
  // Empty.
}

PathScanner::~PathScanner() {
  // Empty.
}

void PathScanner::SetExclusionPatterns(const vector<string>& patterns) {
  mExcludes.assign(patterns.begin(), patterns.end());
}

void PathScanner::Lock() {
  pimpl->Lock();
}

void PathScanner::Unlock() {
  pimpl->Unlock();
}

void PathScanner::Scan(PathTree* tree) {
  static DirectoryIterator end;

  // Check scanability.
  File file(mPath);
  if (!file.exists() || !file.isDirectory() || !file.canRead()) {
    LOG(WARN) << "Cannot scan path " << mPath.toString();
    return;
  }

  // Initialize scanning.
  queue<PathEntry> dirs;
  dirs.push(PathEntry(mPath, 0));

  // Scan loop.
  int count = 0;
  for (; !dirs.empty() && !OverMaxFiles(count); dirs.pop()) {
    Path dir(dirs.front().path);
    for (DirectoryIterator it(dir); it != end && !OverMaxFiles(count); ++it) {
      if (!it->exists() || it->isLink() || !it->canRead()) {
        continue;
      }
      if (it.name()[0] == '.') {
        continue;
      }
      bool excluded = false;
      for (int i = 0; !excluded && i < mExcludes.size(); ++i) {
        excluded |= (it.name() == mExcludes[i]);
      }
      if (excluded) {
        continue;
      }
      Path path = it.path();
      if (it->isDirectory()) {
        path.makeDirectory();
        if (!OverMaxDepth(dirs.front().depth + 1)) {
          dirs.push(PathEntry(it.path(), dirs.front().depth + 1));
        }
      } else {
        tree->Insert(path);
        ++count;
      }
    }
  }
  LOG(INFO) << "Scanned " << count << " files in path " << mPath.toString();
}

void PathScanner::AsyncScan() {
  pimpl->Lock();
  if (!pimpl->mThread) {
    pimpl->mThread = g_thread_create(&PathScanner::Impl::async_scan_thread,
                                     this, true, NULL);
  }
  pimpl->Unlock();
}

void PathScanner::Wait() {
  pimpl->Join();
}

void PathScanner::InternalScan() {
  auto_ptr<PathTree> tree(new PathTree);
  Scan(tree.get());
  pimpl->Lock();
  mTree.reset(tree.release());
  pimpl->Unlock();
}

bool PathScanner::OverMaxFiles(int count) const {
  return mMaxFiles >= 0 && count >= mMaxFiles;
}

bool PathScanner::OverMaxDepth(int depth) const {
  return mMaxDepth >= 0 && depth > mMaxDepth;
}

} // namespace filebow
